Skip to content

feat: trezor hardware support#792

Open
coreyphillips wants to merge 46 commits intomasterfrom
feat/trezor-hardware-support
Open

feat: trezor hardware support#792
coreyphillips wants to merge 46 commits intomasterfrom
feat/trezor-hardware-support

Conversation

@coreyphillips
Copy link

Description

The intention of this Trezor dev dashboard is to serve as a place for developers to test and troubleshoot Trezor hardware features as they emerge. This is not meant to be user facing, but to serve as a reference for how to use and interact with Trezor hardware devices once work on user-facing Trezor features begin by native devs.

  • Implements Trezor hardware support via USB and bluetooth
  • Implements a Trezor dev dashboard that is only be accessible in dev mode at the following location:
    • Settings->Advanced->Trezor

Preview

Screenshot_20260218-093443

QA Notes

  • The latest bindings containing Trezor hardware support can be found here.
  • If you require an apk please let me know and I can build and send one for testing.

- Implement trezor hardware support via USB and Bluetooth
Copy link

@github-advanced-security github-advanced-security bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

detekt found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

@coreyphillips coreyphillips self-assigned this Feb 18, 2026
@coreyphillips coreyphillips added the enhancement New feature or request label Feb 18, 2026
@coreyphillips coreyphillips marked this pull request as draft March 3, 2026 18:34
coreyphillips and others added 3 commits March 5, 2026 12:51
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Bumps bitkit-core version to 0.1.44
- Adds SendTransactionSection.kt
@coreyphillips coreyphillips marked this pull request as ready for review March 7, 2026 18:48
@jvsena42 jvsena42 added this to the 2.2.0 milestone Mar 9, 2026
@jvsena42 jvsena42 self-requested a review March 9, 2026 12:59
jvsena42 added 2 commits March 9, 2026 10:01
# Conflicts:
#	app/src/main/java/to/bitkit/ui/ContentView.kt
#	app/src/main/java/to/bitkit/ui/settings/AdvancedSettingsScreen.kt
#	app/src/main/java/to/bitkit/ui/settings/AdvancedSettingsViewModel.kt
Copy link
Member

@jvsena42 jvsena42 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found some UI guideline issues so far. It is strange that your AI didn't use the AGENTS.md instructions

@Suppress("LongParameterList")
@Composable
private fun TrezorContent(
trezorState: TrezorState,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unstable parameter

@coreyphillips coreyphillips requested a review from jvsena42 March 9, 2026 17:00
Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review

8 issues found (3 bugs, 5 CLAUDE.md violations). Checked for bugs and CLAUDE.md compliance.

Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review

8 issues found (3 bugs, 5 CLAUDE.md violations). Checked for bugs and CLAUDE.md compliance.

endpoint.direction == UsbConstants.USB_DIR_IN -> {
readEndpoint = endpoint
}
endpoint.type == UsbConstants.USB_ENDPOINT_XFER_INT &&
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: bulkTransfer() called on interrupt endpoints - all USB reads/writes will fail

findUsbEndpoints() exclusively selects endpoints with type USB_ENDPOINT_XFER_INT (interrupt), but both readUsbChunk and writeUsbChunk call UsbDeviceConnection.bulkTransfer() on those endpoints. Per Android's documented API contract, bulkTransfer() is only valid for bulk-type endpoints (USB_ENDPOINT_XFER_BULK); calling it with interrupt endpoints returns -1 on every invocation.

This means all USB communication with the Trezor will silently fail.

Fix: Either change findUsbEndpoints to filter for USB_ENDPOINT_XFER_BULK (if the Trezor exposes bulk endpoints on the active interface), or switch to UsbRequest with queue()/requestWait() for proper interrupt transfer handling.


val disconnected = disconnectLatch.await(DISCONNECT_TIMEOUT_MS, TimeUnit.MILLISECONDS)
if (!disconnected) {
Logger.warn("BLE disconnect timeout, forcing close: '$path'", context = TAG)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: userInitiatedCloseSet leaks path on disconnect timeout, silently suppressing future external disconnects

closeBleDevice() adds path to userInitiatedCloseSet at line ~829, relying on onConnectionStateChange to remove it. However, when disconnectLatch.await() times out (the if (!disconnected) branch here), gatt.close() is called immediately after - which can prevent the GATT callback from ever firing for this disconnect. The path then remains in userInitiatedCloseSet indefinitely.

On the next reconnect + external disconnect cycle for the same device path, onConnectionStateChange calls userInitiatedCloseSet.remove(path) which returns true (stale entry), suppressing _externalDisconnect.tryEmit(path). The app's state machine will permanently believe the device is connected.

Fix: Ensure userInitiatedCloseSet.remove(path) always runs after the disconnect attempt, regardless of timeout or exception, by adding it to a finally block after the latch await.

"Scan found ${scannedDevices.size} devices: ${scannedDevices.map { it.id }}",
)
val exactMatch = scannedDevices.find { it.id == deviceId }
val usbDevice = scannedDevices.find { it.transportType == TrezorTransportType.USB }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security bug: connectKnownDevice can silently connect to a different Trezor device

When the known device is Bluetooth and a USB device is visible, the code substitutes the first USB device found in scan results with no identity verification:

val usbDevice = scannedDevices.find { it.transportType == TrezorTransportType.USB }

This selects any USB device - not necessarily the same Trezor. In a multi-device environment or with a malicious USB accessory, this would connect to the wrong hardware wallet, sign Bitcoin transactions with the wrong seed, and overwrite the stored known device record with the substitute's ID.

Compare with autoReconnect() which correctly constrains the USB fallback to known device IDs (it.id in knownIds). The same guard should apply here, or cross-transport identity verification (e.g., matching device label/model after connecting) should be performed before trusting the substituted device.

synchronized(pairingCodeLock) {
submittedPairingCode = ""
pairingCodeRequest = PairingCodeRequest(isRequested = true, latch = latch)
_needsPairingCode.value = true
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CLAUDE.md violation: _stateFlow.value = used instead of _stateFlow.update {}

Per CLAUDE.md: "ALWAYS use _uiState.update { }, NEVER use _stateFlow.value ="

_needsPairingCode.value = is assigned directly in 4 places:

  • Line 294: _needsPairingCode.value = true
  • Line 303: _needsPairingCode.value = false
  • Line 312: _needsPairingCode.value = false
  • Line 349: _needsPairingCode.value = false

All four should use .update { } instead, e.g.:

_needsPairingCode.update { true }
_needsPairingCode.update { false }

private suspend fun connectWithThpRetry(deviceId: String): TrezorFeatures {
TrezorDebugLog.log("THPRetry", "First connect attempt for: $deviceId")
logCredentialFileState(deviceId, "BEFORE 1st attempt")
return try {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CLAUDE.md violation: try-catch used instead of the Result API

Per CLAUDE.md: "ALWAYS use the Result API instead of try-catch" and "NEVER use Exception directly, use AppError instead"

connectWithThpRetry uses a raw try { } catch (e: Exception) { } block. The @Suppress("TooGenericExceptionCaught") annotation confirms the author was aware of the broad catch. This should be refactored to use runCatching { } with Result API chaining (onSuccess, onFailure, getOrElse).

Related: isRetryableError(e: Exception) at line ~623 also uses Exception as a parameter type directly, which should be AppError per CLAUDE.md.

suspend fun autoReconnect(walletIndex: Int = 0): Result<TrezorFeatures> = withContext(bgDispatcher) {
val knownDevices = _state.value.knownDevices.ifEmpty { loadKnownDevices() }
if (knownDevices.isEmpty()) {
return@withContext Result.failure(IllegalStateException("No known devices"))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CLAUDE.md violation: IllegalStateException used instead of AppError

Per CLAUDE.md: "NEVER use Exception directly, use AppError instead" and "ALWAYS inherit custom exceptions from AppError"

IllegalStateException is a raw JVM exception, not an AppError subclass. Two occurrences:

  • Line 381 (in autoReconnect): Result.failure(IllegalStateException("No known devices"))
  • Line 413 (in connectKnownDevice): Result.failure(IllegalStateException("Connection already in progress"))

Both should use AppError (or a custom subclass) instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants